require 'erb'
require 'common/base'
Dir.glob("components/*/*.{rb}").each {|c| require c}

class MuSLBuilder
#  include MuslMessage

 attr_accessor :sequences, :messages, :base, :output_file, :output

 def initialize(args)
    @step = 1
    @params = Hash.new
    @components = nil
    @output_file = nil
    @sequences = Array.new
    @transports = Array.new
    @additional_transports = Array.new
    @hosts = Array.new
    @passed_in_options = Array.new
    @messages = Array.new
    @base_classes = Hash.new
    @component_list = []
    @component_map = {}
    @scenario_name = "default scenario name"
    @steps = ""
    @output = ""
    @footer = "    }
}"

    # the argument passed in to the constructor is either a properties file or an array
    @properties = get_args_array(args)
    @properties.each do | p |
      if p.match("^messages")
        @messages = p.split("=")[1].strip.chomp
      elsif p.match("^sequence")
         @sequences << p.split("=")[1].strip.chomp
      elsif p.match("^transport") # additional transports from properties file 
         @transports << p.split("=")[1].strip.chomp
      elsif p.match("^additional_transport") # additional transports from builder ui
         @additional_transports << p.split("=")[1].strip.chomp
      elsif p.match("^host") # additional hosts from builder ui
         @hosts << p.split("=", 2)[1].strip.chomp
      elsif p.match("^option") # additional options from builder ui
         @options << p.split("=", 2)[1].strip.chomp
      elsif p.match("^scenario_name")
         @scenario_name = p.split("=")[1].strip.chomp
      elsif p.match("^components")
         @components = p.split("=")[1].strip.chomp
         @component_list = @components.split(",")
      elsif p.match("^output_file")
         @output_file = p.split("=")[1].strip.chomp unless p.split("=")[1].nil?
      else # component-specific parameters ("options") are put into the params map
         str = p.split("=")
         @passed_in_options << p
         key = str[0].strip
         value = str[1].strip.chomp unless str[1].nil?
         @params[key] = value unless value.nil?
      end
    end

    # instantiate the global and component base classes
    # @global_base = GlobalBase.new
    unless @component_list.length == 0
     @component_list.each do | c |
       klass = "#{c}Base"
       # base = eval(klass).new(@global_base, params)
       # @base_classes << base
       # map the component name to its base class instance
       @component_map[c] = klass
     end
     @output = go
    else
      puts "error: properties is missing the required 'components' definition"
    end
 end

 # the argument passed in to the constructor is either a properties file or an array
 def get_args_array(args)
   if !args.is_a?(Array)
     arr = []
     f = File.open(args, "r")
     f.each_line do |line|
       arr.push line
     end
     return arr
   else
     return args
   end
 end

 # main routine
 def go
  output = ""
  make_sequence_map
  make_transports_map
  build_messages
  output = get_scenario

  if @output_file.nil?
     puts output
  else
    File.open(@output_file, 'w') do |f2|
      f2.puts output
    end
  end
  return output
 end

 # compile the header using all of the components' base classes' options,
 # variables, and transports

 # TODO
=begin
        <% additional_options.each do | o | %>
          <%= o %>
        <% end %>
=end
 
 def make_header
#   additional_options = get_extra_options
   template = '
  scenario(name: "<%= @scenario_name %>")
  {

    hosts {
       <% @base_classes.each do | k, v | %>
         <%= v.get_hosts %>
       <% end %>
       <% @hosts.each do | h | %>
          <%= h %>
        <% end %>
    }

    options {
       <% @base_classes.each do | k, v | %>
         <%= v.get_options %>
       <% end %>
    }

    variables {
       <% @base_classes.each do | k, v | %>
         <%= v.get_variables %>
       <% end %>
    }

    steps {
        <% @base_classes.each do | k, v | %>
          <%= v.get_transports %>
        <% end %>
        <% @additional_transports.each do | t | %>
          <% p = t.split(":", 2) %>
          <% str = p[0] + " = " + p[1] %>
          <%= str %>
        <% end %>
    '

     erb = ERB.new(template, 0, '%<>')
     @header = erb.result(binding)
 end

 # add each sequence to the global sequence map
 def make_sequence_map
    @sequence_map = Hash.new
    if @sequences.size > 0
      @sequences.each do | s |
         p = s.split(":")
         @sequence_map[p[0]] = p[1]
      end
    end
 end

 # add each properties-file and builder-ui-defined transport to the global transports map
 # converting the name:value pair into a "name = value" string for the hash value
 # while keeping the name for the hash key
 def make_transports_map
    @transport_map = Hash.new
    if @transports.size > 0
      @transports.each do | s |
         p = s.split(":", 2)
         @transport_map[p[0]] = p[0] + " = " + p[1]
      end
    end
    if @additional_transports.size > 0
      @additional_transports.each do | s |
         p = s.split(":", 2)
         @transport_map[p[0]] = p[0] + " = " + p[1]
      end
    end
 end

 # TODO; not working properly. to be fixed
 # compare any base class options with options passed in from properties or builder ui
 # and return an array of new passed_in ones.
 def get_extra_options
   base_class_options = Array.new
   diff_options = Array.new
   final_options = Array.new
   $DEBUG and puts "\nbase class options\n\n"
   @base_classes.each do | k, v |
     if !v.nil?
       v.get_options.each do | o |
        str = o.split("=")[0].strip
        base_class_options << str[1,str.length]
        $DEBUG and puts str
       end
     end
   end

   $DEBUG and puts "\npassed in options\n\n"
   passed_options = Array.new
   @passed_in_options.each do | p |
     s =  p.split("=")[0].strip
     $DEBUG and puts s
     passed_options << s
   end

   diff_options = passed_options - base_class_options

   $DEBUG and puts "\nfinal options\n\n"
   @passed_in_options.each do | p |
     diff_options.each do | o |
       $DEBUG and puts o
       $DEBUG and puts p
       if p.include?(o)
         final_options << "$" + p
         $DEBUG and puts p
       end 
     end
   end
   return final_options
 end

 # process the messages value, splitting sequences into their constituent messages along the way
 def build_messages
   msgs = @messages.split(",")
    msgs.each do | m |
      n = get_name(m)
      if !@sequence_map[n].nil?  # if it's a sequence, process its messages
        num, m = get_repeats(m) # sequences may be repeated as well as messages
        seq_msgs = @sequence_map[m].split(",")
        num.times do
          seq_msgs.each do | sm |
            add_message_to_steps(sm)
          end
        end
      else  # it's a single message, process it directly
        add_message_to_steps(m)
      end # if
    end # msgs.each do
 end

 # add the message to the scenario steps by
 # instantiating the message class and fetching its 'message' string
 def add_message_to_steps(m)
   
    # 'm' might be a transport, in which case, add it to the steps and return
    t = check_new_transport(m)
    if !t.nil?
       @steps = @steps + "\n\n" + t
       return
    end

    transport, m = get_alternate_transport(m)  # message name followed by alternate transport in square brackets
    num, m = get_repeats(m)
    num.times do
      # klass = "MuslMessage::" + m
      begin
        # instantiate the component base_class with the message
        # so it can create an instance of the message
        klass = get_base_class(m)

       if klass.nil?
         raise "A base class was not found for the message #{m}. Please check the message name to make sure it exists, is named correctly ('<ComponentName><MessageName>.txt'), and is located in its component's directory"
       end
# puts m
        if transport.nil?
          instance = eval(klass).new(@step, m, @params)
        else # there is an alternate transport specified (message_class[alt_transport])
          instance = eval(klass).new(@step, m, @params, transport)
        end
        @steps = @steps + "\n\n" + instance.message
        # add one instance of each component to the @base_classes map, for header_making ...
        if @base_classes[klass].nil?
          @base_classes[klass] = instance
        end
        # increment the global step counter
        @step = @step + 1
      rescue NameError => e
          puts "NameError: There is a problem making the class #{klass}, for the message #{m}. Possible reasons include: the base class is not named correctly, the properties file 'components' and/or 'messages' values may be invalid, or the message template may contain embedded ruby errors"
      end # begin Kernel.const
    end # num.times do
  end

 # get the component base class for the message so it
 # can be passed to the message class' constructor
 def get_base_class(message_class)
   @component_list.each do | c |
     if message_class.index(c) == 0
       return @component_map[c]
     end
   end
   return nil
 end

 # make the header and add the steps and footer
 # returns the entire MuSL document
 def get_scenario
    make_header
    return "#{@header}\n#{@steps}\n#{@footer}"
 end

  # num = number of times to repeat the message
  # if the message is in the form of Message(N), then N is the number of times to repeat
  def get_repeats(m="")
    num = 1
    if !m.index('(').nil?
      num = m[m.rindex('(')+1..m.rindex(')')-1].to_i
      m = m[0, m.rindex('(')]
    end
    return num, m
  end

  # 'm' could be the key in the transport map
  # in which case, return its value. else return nil
  def check_new_transport(m="")
   @transport_map.each do | k, v |
     if m.index(k) == 0
       return v
     end
   end
   return nil
  end

  # if the message is in the form of Message[X], then X is the alternate transport to use
  def get_alternate_transport(m="")
    x = nil
    if !m.index('[').nil?
      x = m[m.rindex('[')+1..m.rindex(']')-1]
      m = m[0, m.rindex('[')]
    end
    return x, m
  end

  # if the name contains parens (for repeats), strips them off
  # else returns the original name
  def get_name(m)
    if !m.index('(').nil?
      m = m[0, m.rindex('(')]
    end
    return m
  end
  
end

# the command-line executable
if $0 == __FILE__
  if ARGV[0].nil?
    puts "Usage: <properties_file>"
  else #  properties file is required arg
   fb = MuSLBuilder.new(ARGV[0])
  end
end